/******************************************************************************
 * Copyright (c) 2016 TypeFox and others.
 * 
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 * 
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 ******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.eclipse.lsp4j.jsonrpc.Endpoint;
import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod;
import org.eclipse.lsp4j.jsonrpc.json.ResponseJsonAdapter;

import com.google.gson.TypeAdapterFactory;

public final class ServiceEndpoints {
	private ServiceEndpoints() {}
	
	/**
	 * Wraps a given {@link Endpoint} in the given service interface.
	 * 
	 * @return the wrapped service object
	 */
	@SuppressWarnings("unchecked")
	public static <T> T toServiceObject(Endpoint endpoint, Class<T> interface_) {
		Class<?>[] interfArray = new Class[]{interface_, Endpoint.class};
		EndpointProxy invocationHandler = new EndpointProxy(endpoint, interface_);
		return (T) Proxy.newProxyInstance(interface_.getClassLoader(), interfArray, invocationHandler);
	}
	
	/**
	 * Wraps a given {@link Endpoint} in the given service interfaces.
	 * 
	 * @return the wrapped service object
	 */
	public static Object toServiceObject(Endpoint endpoint, Collection<Class<?>> interfaces, ClassLoader classLoader) {
		Class<?>[] interfArray = new Class[interfaces.size() + 1];
		interfaces.toArray(interfArray);
		interfArray[interfArray.length - 1] = Endpoint.class;
		EndpointProxy invocationHandler = new EndpointProxy(endpoint, interfaces);
		return Proxy.newProxyInstance(classLoader, interfArray, invocationHandler);
	}
	
	/**
	 * Wraps a given object with service annotations behind an {@link Endpoint} interface.
	 * 
	 * @return the wrapped service endpoint
	 */
	public static Endpoint toEndpoint(Object serviceObject) {
		return new GenericEndpoint(serviceObject);
	}
	
	/**
	 * Wraps a collection of objects with service annotations behind an {@link Endpoint} interface.
	 * 
	 * @return the wrapped service endpoint
	 */
	public static Endpoint toEndpoint(Collection<Object> serviceObjects) {
		return new GenericEndpoint(serviceObjects);
	}
	
	/**
	 * Finds all Json RPC methods on a given class.
	 * 
	 * @return the supported JsonRpcMethods
	 */
	public static Map<String, JsonRpcMethod> getSupportedMethods(Class<?> type) {
		Set<Class<?>> visitedTypes = new HashSet<>();
		return getSupportedMethods(type, visitedTypes);
	}
	
	/**
	 * Finds all Json RPC methods on a given type
	 */
	private static Map<String, JsonRpcMethod> getSupportedMethods(Class<?> type, Set<Class<?>> visitedTypes) {
		Map<String, JsonRpcMethod> result = new LinkedHashMap<>();
		AnnotationUtil.findRpcMethods(type, visitedTypes, methodInfo -> {
			JsonRpcMethod meth;
			if (methodInfo.isNotification) {
				meth = JsonRpcMethod.notification(methodInfo.name, methodInfo.parameterTypes);
			} else {
				Type genericReturnType = methodInfo.method.getGenericReturnType();
				if (genericReturnType instanceof ParameterizedType) {
					Type returnType = ((ParameterizedType) genericReturnType).getActualTypeArguments()[0];
					TypeAdapterFactory responseTypeAdapter = null;
					ResponseJsonAdapter responseTypeAdapterAnnotation = methodInfo.method.getAnnotation(ResponseJsonAdapter.class);
					if (responseTypeAdapterAnnotation != null) {
						try {
							responseTypeAdapter = responseTypeAdapterAnnotation.value().getDeclaredConstructor().newInstance();
						} catch (ReflectiveOperationException e) {
							throw new RuntimeException(e);
						}
					}
					meth = JsonRpcMethod.request(methodInfo.name, returnType, responseTypeAdapter, methodInfo.parameterTypes);
				} else {
					throw new IllegalStateException("Expecting return type of CompletableFuture but was : " + genericReturnType);
				}
			}
			if (result.put(methodInfo.name, meth) != null) {
				throw new IllegalStateException("Duplicate RPC method "+methodInfo.name+".");
			}
		});
		
		AnnotationUtil.findDelegateSegments(type, new HashSet<>(), method -> {
			Map<String, JsonRpcMethod> supportedDelegateMethods = getSupportedMethods(method.getReturnType(), visitedTypes);
			for (JsonRpcMethod meth : supportedDelegateMethods.values()) {
				if (result.put(meth.getMethodName(), meth) != null) {
					throw new IllegalStateException("Duplicate RPC method "+meth.getMethodName()+".");
				}
			}
		});
		return result;
	}
	
}
